Commit a35a9890b00b538cd0f3ef94a526c0dd2ec461bf

Edward Thomson 2014-07-17T18:25:03

Introduce git_rebase_commit Commit the current patch of a rebase process.

diff --git a/include/git2/rebase.h b/include/git2/rebase.h
index e4300eb..0fe2b26 100644
--- a/include/git2/rebase.h
+++ b/include/git2/rebase.h
@@ -82,6 +82,34 @@ GIT_EXTERN(int) git_rebase_next(
 	git_checkout_options *checkout_opts);
 
 /**
+ * Commits the current patch.  You must have resolved any conflicts that
+ * were introduced during the patch application from the `git_rebase_next`
+ * invocation.
+ * 
+ * @param id Pointer in which to store the OID of the newly created commit
+ * @param repo The repository with the in-progress rebase
+ * @param author The author of the updated commit, or NULL to keep the
+ *        author from the original commit
+ * @param committer The committer of the rebase
+ * @param message_encoding The encoding for the message in the commit,
+ *        represented with a standard encoding name.  If message is NULL,
+ *        this should also be NULL, and the encoding from the original
+ *        commit will be maintained.  If message is specified, this may be
+ *        NULL to indicate that "UTF-8" is to be used.
+ * @param message The message for this commit, or NULL to keep the message
+ *        from the original commit
+ * @return Zero on success, GIT_EUNMERGED if there are unmerged changes in
+ *        the index, -1 on failure.
+ */
+GIT_EXTERN(int) git_rebase_commit(
+	git_oid *id,
+	git_repository *repo,
+	const git_signature *author,
+	const git_signature *committer,
+	const char *message_encoding,
+	const char *message);
+
+/**
  * 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 676a803..9245dca 100644
--- a/src/rebase.c
+++ b/src/rebase.c
@@ -33,6 +33,7 @@
 #define END_FILE			"end"
 #define CMT_FILE_FMT		"cmt.%d"
 #define CURRENT_FILE		"current"
+#define REWRITTEN_FILE		"rewritten"
 
 #define ORIG_DETACHED_HEAD	"detached HEAD"
 
@@ -284,7 +285,7 @@ static int rebase_finish(git_rebase_state *state)
 		0;
 }
 
-static int rebase_setupfile(git_repository *repo, const char *filename, const char *fmt, ...)
+static int rebase_setupfile(git_repository *repo, const char *filename, int flags, const char *fmt, ...)
 {
 	git_buf path = GIT_BUF_INIT,
 		contents = GIT_BUF_INIT;
@@ -297,7 +298,7 @@ static int rebase_setupfile(git_repository *repo, const char *filename, const ch
 
 	if ((error = git_buf_joinpath(&path, repo->path_repository, REBASE_MERGE_DIR)) == 0 &&
 		(error = git_buf_joinpath(&path, path.ptr, filename)) == 0)
-		error = git_futils_writebuffer(&contents, path.ptr, O_RDWR|O_CREAT, REBASE_FILE_MODE);
+		error = git_futils_writebuffer(&contents, path.ptr, flags, REBASE_FILE_MODE);
 
 	git_buf_free(&path);
 	git_buf_free(&contents);
@@ -361,16 +362,16 @@ static int rebase_setup_merge(
 		git_buf_printf(&commit_filename, CMT_FILE_FMT, commit_cnt);
 
 		git_oid_fmt(id_str, &id);
-		if ((error = rebase_setupfile(repo, commit_filename.ptr,
+		if ((error = rebase_setupfile(repo, commit_filename.ptr, -1,
 				"%.*s\n", GIT_OID_HEXSZ, id_str)) < 0)
 			goto done;
 	}
 
 	if (error != GIT_ITEROVER ||
-		(error = rebase_setupfile(repo, END_FILE, "%d\n", commit_cnt)) < 0)
+		(error = rebase_setupfile(repo, END_FILE, -1, "%d\n", commit_cnt)) < 0)
 		goto done;
 
-	error = rebase_setupfile(repo, ONTO_NAME_FILE, "%s\n",
+	error = rebase_setupfile(repo, ONTO_NAME_FILE, -1, "%s\n",
 		rebase_onto_name(onto));
 
 done:
@@ -405,10 +406,10 @@ static int rebase_setup(
 
 	orig_head_name = branch->ref_name ? branch->ref_name : ORIG_DETACHED_HEAD;
 
-	if ((error = rebase_setupfile(repo, HEAD_NAME_FILE, "%s\n", orig_head_name)) < 0 ||
-		(error = rebase_setupfile(repo, ONTO_FILE, "%s\n", onto->oid_str)) < 0 ||
-		(error = rebase_setupfile(repo, ORIG_HEAD_FILE, "%s\n", branch->oid_str)) < 0 ||
-		(error = rebase_setupfile(repo, QUIET_FILE, opts->quiet ? "t\n" : "\n")) < 0)
+	if ((error = rebase_setupfile(repo, HEAD_NAME_FILE, -1, "%s\n", orig_head_name)) < 0 ||
+		(error = rebase_setupfile(repo, ONTO_FILE, -1, "%s\n", onto->oid_str)) < 0 ||
+		(error = rebase_setupfile(repo, ORIG_HEAD_FILE, -1, "%s\n", branch->oid_str)) < 0 ||
+		(error = rebase_setupfile(repo, QUIET_FILE, -1, opts->quiet ? "t\n" : "\n")) < 0)
 		goto done;
 
 	error = rebase_setup_merge(repo, branch, upstream, onto, opts);
@@ -615,8 +616,8 @@ static int rebase_next_merge(
 			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)
+	if ((error = rebase_setupfile(repo, MSGNUM_FILE, -1, "%d\n", state->merge.msgnum)) < 0 ||
+		(error = rebase_setupfile(repo, CURRENT_FILE, -1, "%s\n", current.ptr)) < 0)
 		goto done;
 
 	if ((error = normalize_checkout_opts(repo, &checkout_opts, given_checkout_opts, state)) < 0 ||
@@ -661,6 +662,104 @@ int git_rebase_next(
 	return error;
 }
 
+static int rebase_commit_merge(
+	git_oid *commit_id,
+	git_repository *repo,
+	git_rebase_state *state,
+	const git_signature *author,
+	const git_signature *committer,
+	const char *message_encoding,
+	const char *message)
+{
+	git_index *index = NULL;
+	git_reference *head = NULL;
+	git_commit *head_commit = NULL;
+	git_tree *tree = NULL;
+	git_oid tree_id;
+	char old_idstr[GIT_OID_HEXSZ], new_idstr[GIT_OID_HEXSZ];
+	int error;
+
+	if (!state->merge.msgnum || !state->merge.current) {
+		giterr_set(GITERR_REBASE, "No rebase-merge state files exist");
+		error = -1;
+		goto done;
+	}
+
+	if ((error = git_repository_index(&index, repo)) < 0)
+		goto done;
+
+	if (git_index_has_conflicts(index)) {
+		giterr_set(GITERR_REBASE, "Conflicts have not been resolved");
+		error = GIT_EMERGECONFLICT;
+		goto done;
+	}
+
+	/* TODO: if there are no changes, error with a useful code */
+
+	if ((error = git_repository_head(&head, repo)) < 0 ||
+		(error = git_reference_peel((git_object **)&head_commit, head, GIT_OBJ_COMMIT)) < 0 ||
+		(error = git_index_write_tree(&tree_id, index)) < 0 ||
+		(error = git_tree_lookup(&tree, repo, &tree_id)) < 0)
+		goto done;
+
+	if (!author)
+		author = git_commit_author(state->merge.current);
+
+	if (!message) {
+		message_encoding = git_commit_message_encoding(state->merge.current);
+		message = git_commit_message(state->merge.current);
+	}
+
+	if ((error = git_commit_create(commit_id, repo, "HEAD", author,
+			committer, message_encoding, message, tree, 1,
+			(const git_commit **)&head_commit)) < 0)
+		goto done;
+
+	git_oid_fmt(old_idstr, git_commit_id(state->merge.current));
+	git_oid_fmt(new_idstr, commit_id);
+
+	error = rebase_setupfile(repo, REWRITTEN_FILE, O_CREAT|O_WRONLY|O_APPEND,
+		"%.*s %.*s\n", GIT_OID_HEXSZ, old_idstr, GIT_OID_HEXSZ, new_idstr);
+
+done:
+	git_tree_free(tree);
+	git_commit_free(head_commit);
+	git_reference_free(head);
+	git_index_free(index);
+
+	return error;
+}
+
+int git_rebase_commit(
+	git_oid *id,
+	git_repository *repo,
+	const git_signature *author,
+	const git_signature *committer,
+	const char *message_encoding,
+	const char *message)
+{
+	git_rebase_state state = GIT_REBASE_STATE_INIT;
+	int error;
+
+	assert(repo && committer);
+
+	if ((error = rebase_state(&state, repo)) < 0)
+		goto done;
+
+	switch (state.type) {
+	case GIT_REBASE_TYPE_MERGE:
+		error = rebase_commit_merge(
+			id, repo, &state, author, committer, message_encoding, message);
+		break;
+	default:
+		abort();
+	}
+
+done:
+	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/src/signature.c b/src/signature.c
index 514b153..818cd30 100644
--- a/src/signature.c
+++ b/src/signature.c
@@ -270,3 +270,14 @@ void git_signature__writebuf(git_buf *buf, const char *header, const git_signatu
 			(unsigned)sig->when.time, sign, hours, mins);
 }
 
+bool git_signature__equal(const git_signature *one, const git_signature *two)
+{
+	assert(one && two);
+
+	return
+		git__strcmp(one->name, two->name) == 0 &&
+		git__strcmp(one->email, two->email) == 0 &&
+		one->when.time == two->when.time &&
+		one->when.offset == two->when.offset;
+}
+
diff --git a/src/signature.h b/src/signature.h
index eb71db7..75265df 100644
--- a/src/signature.h
+++ b/src/signature.h
@@ -14,6 +14,7 @@
 
 int git_signature__parse(git_signature *sig, const char **buffer_out, const char *buffer_end, const char *header, char ender);
 void git_signature__writebuf(git_buf *buf, const char *header, const git_signature *sig);
+bool git_signature__equal(const git_signature *one, const git_signature *two);
 
 int git_signature__pdup(git_signature **dest, const git_signature *source, git_pool *pool);
 
diff --git a/tests/rebase/merge.c b/tests/rebase/merge.c
index 506c411..e578bef 100644
--- a/tests/rebase/merge.c
+++ b/tests/rebase/merge.c
@@ -1,6 +1,7 @@
 #include "clar_libgit2.h"
 #include "git2/rebase.h"
 #include "posix.h"
+#include "signature.h"
 
 #include <fcntl.h>
 
@@ -11,10 +12,13 @@ static git_signature *signature;
 void test_rebase_merge__initialize(void)
 {
 	repo = cl_git_sandbox_init("rebase");
+	cl_git_pass(git_signature_new(&signature,
+		"Rebaser", "rebaser@rebaser.rb", 1405694510, 0));
 }
 
 void test_rebase_merge__cleanup(void)
 {
+	git_signature_free(signature);
 	cl_git_sandbox_cleanup();
 }
 
@@ -116,3 +120,90 @@ void test_rebase_merge__next_with_conflicts(void)
 	git_reference_free(upstream_ref);
 }
 
+void test_rebase_merge__commit(void)
+{
+	git_reference *branch_ref, *upstream_ref;
+	git_merge_head *branch_head, *upstream_head;
+	git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
+	git_oid commit_id, tree_id, parent_id;
+	git_signature *author;
+	git_commit *commit;
+
+	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_git_pass(git_rebase_commit(&commit_id, repo, NULL, signature,
+		NULL, NULL));
+
+	cl_git_pass(git_commit_lookup(&commit, repo, &commit_id));
+
+	git_oid_fromstr(&parent_id, "efad0b11c47cb2f0220cbd6f5b0f93bb99064b00");
+	cl_assert_equal_i(1, git_commit_parentcount(commit));
+	cl_assert_equal_oid(&parent_id, git_commit_parent_id(commit, 0));
+
+	git_oid_fromstr(&tree_id, "4461379789c777d2a6c1f2ee0e9d6c86731b9992");
+	cl_assert_equal_oid(&tree_id, git_commit_tree_id(commit));
+
+	cl_assert_equal_s(NULL, git_commit_message_encoding(commit));
+	cl_assert_equal_s("Modification 1 to beef\n", git_commit_message(commit));
+
+	cl_git_pass(git_signature_new(&author,
+		"Edward Thomson", "ethomson@edwardthomson.com", 1405621769, 0-(4*60)));
+	cl_assert(git_signature__equal(author, git_commit_author(commit)));
+
+	cl_assert(git_signature__equal(signature, git_commit_committer(commit)));
+
+	cl_assert_equal_file("da9c51a23d02d931a486f45ad18cda05cf5d2b94 776e4c48922799f903f03f5f6e51da8b01e4cce0\n", 82, "rebase/.git/rebase-merge/rewritten");
+
+	git_signature_free(author);
+	git_commit_free(commit);
+	git_merge_head_free(branch_head);
+	git_merge_head_free(upstream_head);
+	git_reference_free(branch_ref);
+	git_reference_free(upstream_ref);
+}
+
+void test_rebase_merge__commit_updates_rewritten(void)
+{
+	git_reference *branch_ref, *upstream_ref;
+	git_merge_head *branch_head, *upstream_head;
+	git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
+	git_oid commit_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_git_pass(git_rebase_commit(&commit_id, repo, NULL, signature,
+		NULL, NULL));
+
+	cl_git_pass(git_rebase_next(repo, &checkout_opts));
+	cl_git_pass(git_rebase_commit(&commit_id, repo, NULL, signature,
+		NULL, NULL));
+
+	cl_assert_equal_file(
+		"da9c51a23d02d931a486f45ad18cda05cf5d2b94 776e4c48922799f903f03f5f6e51da8b01e4cce0\n"
+		"8d1f13f93c4995760ac07d129246ac1ff64c0be9 ba1f9b4fd5cf8151f7818be2111cc0869f1eb95a\n",
+		164, "rebase/.git/rebase-merge/rewritten");
+
+	git_merge_head_free(branch_head);
+	git_merge_head_free(upstream_head);
+	git_reference_free(branch_ref);
+	git_reference_free(upstream_ref);
+}
+